use crate::{
    context::Context,
    mesh::Mesh,
    instance::{Instance, InstanceArray},
    texture::Texture,
    uniform::Uniform,
    camera::{Camera, CameraUniform},
    model_loader,
    control::CameraControl
};

pub trait State {
    fn get_window(&self) -> &winit::window::Window;
    fn reconfigure(&self);
    fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>);
    fn input(&mut self, event: &winit::event::WindowEvent) -> bool;
    fn update(&mut self);
    fn render(&mut self) -> Result<(), wgpu::SurfaceError>;
}

pub struct MyState {
    ctx: Context,

    depth_texture_view: wgpu::TextureView,

    mesh: Mesh,

    instance_array: InstanceArray,

    diffuse_texture: Texture,

    camera: Camera,
    camera_uniform: Uniform<CameraUniform>,

    render_pipeline: wgpu::RenderPipeline,

    camera_control: CameraControl
}

impl MyState {
    pub async fn new(window: winit::window::Window) -> Self {
        // configure

        let ctx = Context::new(window).await;
        let device = &ctx.device;
        let queue = &ctx.queue;
        let config = &ctx.config;

        // depth

        let depth_texture = Texture::new_depth_texture(
            device,
            config.width,
            config.height
        );

        // mesh

        let mut mesh_data = model_loader::load_model(
            "/run/media/sunny/data/datasets/cirno_fumo/cirno_low.obj"
        ).unwrap();

        for vertex in &mut mesh_data.vertices {
            vertex.position[0] *= 0.02;
            vertex.position[1] *= 0.02;
            vertex.position[2] *= 0.02;
        }

        let mesh = Mesh::from_data(&device, &mesh_data);

        // instance

        const NUM_INSTANCES_PER_ROW: u32 = 10;
        const INSTANCE_DISPLACEMENT: cgmath::Vector3<f32> = cgmath::Vector3::new(
            NUM_INSTANCES_PER_ROW as f32 * 0.5,
            0.0,
            NUM_INSTANCES_PER_ROW as f32 * 0.5
        );

        let instances = (0..NUM_INSTANCES_PER_ROW).flat_map(|z| {
            (0.. NUM_INSTANCES_PER_ROW).map(move |x| {
                use cgmath::prelude::*;

                let position = cgmath::Vector3::new(x as f32, 0.0, z as f32) - INSTANCE_DISPLACEMENT;

                let rotation = if position.is_zero() {
                    cgmath::Quaternion::from_axis_angle(cgmath::Vector3::unit_z(), cgmath::Deg(0.0))
                } else {
                    cgmath::Quaternion::from_axis_angle(position.normalize(), cgmath::Deg(45.0))
                };

                Instance { position, rotation }
            })
        }).collect::<Vec<_>>();

        let instance_data = instances.iter().map(Instance::to_raw).collect::<Vec<_>>();

        let instance_array = InstanceArray::from_data(device, &instance_data);

        // texture

        let texture_bind_group_layout = Texture::bind_group_layout(device);

        let diffuse_image = model_loader::load_image(
            "/run/media/sunny/data/datasets/cirno_fumo/cirno_low_u1_v1.jpg"
        ).unwrap();

        let diffuse_texture = Texture::from_image(
            device,
            queue,
            &texture_bind_group_layout,
            &diffuse_image
        );

        // camera

        let camera_bind_group_layout = Uniform::bind_group_layout(
            device,
            CameraUniform::VISIBILITY,
            "Camera Uniform Bind Group Layout"
        );

        let camera = Camera {
            pos: (0.0, 0.5, 1.0).into(),
            up: cgmath::Vector3::unit_y(),
            yaw: cgmath::Deg(-90.0),
            pitch: cgmath::Deg(0.0),
            speed: 1.0,
            rot_speed: cgmath::Deg(70.0),
            fovy: cgmath::Deg(45.0).into(),
            aspect: config.width as f32 / config.height as f32,
            near: 0.1,
            far: 100.0
        };

        let camera_uniform = Uniform::new(
            device,
            &camera_bind_group_layout,
            &CameraUniform::new(&camera),
            "Camera Uniform Buffer",
            "Camera Uniform Bind Group"
        );

        // pipeline

        let shader = device.create_shader_module(wgpu::include_wgsl!("shader.wgsl"));

        let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("Render Pipeline Layout"),
            bind_group_layouts: &[
                &texture_bind_group_layout,
                &camera_bind_group_layout
            ],
            push_constant_ranges: &[]
        });

        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("Render Pipeline"),
            layout: Some(&render_pipeline_layout),
            vertex: wgpu::VertexState {
                module: &shader,
                entry_point: "vs_main",
                buffers: &[
                    mesh.layout.clone(),
                    instance_array.layout.clone()
                ]
            },
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: "fs_main",
                targets: &[
                    Some(wgpu::ColorTargetState {
                        format: config.format,
                        blend: Some(wgpu::BlendState::REPLACE),
                        write_mask: wgpu::ColorWrites::ALL
                    })
                ]
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                strip_index_format: None,
                front_face: wgpu::FrontFace::Ccw,
                cull_mode: Some(wgpu::Face::Back),
                polygon_mode: wgpu::PolygonMode::Fill,
                unclipped_depth: false,
                conservative: false
            },
            depth_stencil: Some(wgpu::DepthStencilState {
                format: Texture::DEPTH_FORMAT,
                depth_write_enabled: true,
                depth_compare: wgpu::CompareFunction::Less,
                stencil: Default::default(),
                bias: Default::default()
            }),
            multisample: wgpu::MultisampleState {
                count: 1,
                mask: !0,
                alpha_to_coverage_enabled: false
            },
            multiview: None
        });

        // control

        let camera_control = CameraControl::new();

        Self {
            ctx,
            depth_texture_view: depth_texture,
            mesh,
            diffuse_texture,
            instance_array,
            camera,
            camera_uniform,
            render_pipeline,
            camera_control
        }
    }
}

impl State for MyState {
    fn get_window(&self) -> &winit::window::Window {
        &self.ctx.window
    }

    fn reconfigure(&self) {
        self.ctx.reconfigure();
    }

    fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
        self.ctx.resize(new_size);

        let config = &self.ctx.config;

        self.depth_texture_view = Texture::new_depth_texture(
            &self.ctx.device,
            config.width,
            config.height
        );

        self.camera.aspect = config.width as f32 / config.height as f32;
    }

    fn input(&mut self, event: &winit::event::WindowEvent) -> bool {
        self.camera_control.handle_events(event)
    }

    fn update(&mut self) {
        self.camera_control.update_camera(&mut self.camera);
        self.camera_uniform.update(&self.ctx.queue, &CameraUniform::new(&self.camera));
    }

    fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
        let output = self.ctx.surface.get_current_texture()?;

        let view = output.texture.create_view(&Default::default());

        let mut encoder = self.ctx.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
            label: Some("Render Encoder")
        });

        {
            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("Render Pass"),
                color_attachments: &[
                    Some(wgpu::RenderPassColorAttachment {
                        view: &view,
                        resolve_target: None,
                        ops: wgpu::Operations {
                            load: wgpu::LoadOp::Clear(wgpu::Color {
                                r: 0.1,
                                g: 0.2,
                                b: 0.3,
                                a: 1.0
                            }),
                            store: true
                        }
                    })
                ],
                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
                    view: &self.depth_texture_view,
                    depth_ops: Some(wgpu::Operations {
                        load: wgpu::LoadOp::Clear(1.0),
                        store: true
                    }),
                    stencil_ops: None
                })
            });

            render_pass.set_pipeline(&self.render_pipeline);
            render_pass.set_bind_group(0, &self.diffuse_texture.bind_group, &[]);
            render_pass.set_bind_group(1, &self.camera_uniform.bind_group, &[]);
            render_pass.set_vertex_buffer(0, self.mesh.vertex_buffer.slice(..));
            render_pass.set_vertex_buffer(1, self.instance_array.buffer.slice(..));
            render_pass.set_index_buffer(self.mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
            render_pass.draw_indexed(0..self.mesh.draw_cnt, 0, 0..self.instance_array.cnt);
        }

        self.ctx.queue.submit(std::iter::once(encoder.finish()));
        output.present();

        Ok(())
    }
}
